﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using ModelQuality.Metrics;
using ModelQuality.Metrics.Rules;
using System.Text.RegularExpressions;
using log4net;
using System.Reflection;

namespace ModelQuality.Metrics.Engine
{
    public static class Predicates
    {
        private static readonly ILog log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
        
        public interface IPredicate
        {
            bool CanApply(IList<Object> objs, IList<Parameter> parameters, IValidationContext validationContext);

            bool Apply(IList<Object> objs, IList<Parameter> parameters, IValidationContext validationContext);
        }

        public static IPredicate From(Operation operation)
        {
            IPredicate pred = null;
            switch (operation)
            {
                case Operation.EQUALS:
                    pred = new EqualsPredicate();
                    break;
                case Operation.NOT_EMPTY:
                    pred = new NotEmptyPredicate();
                    break;
                case Operation.NOT_EQUALS:
                    pred = new NotEqualsPredicate();
                    break;
                case Operation.MAX_LENGTH:
                    pred = new MaxLengthPredicate();
                    break;
                case Operation.UNICITY:
                    pred = new UnicityPredicate();
                    break;
                case Operation.REQUIRED:
                    pred = new RequiredPredicate();
                    break;
                case Operation.FORBIDDEN:
                    pred = new ForbiddenPredicate();
                    break;
                case Operation.CONTAINS:
                    pred = new ContainsPredicate();
                    break;
                case Operation.MAX_SIZE:
                    pred = new MaxSizePredicate();
                    break;
            }

            return pred;
        }

        class EqualsPredicate : IPredicate
        {
            #region IPredicate Membres

            public bool CanApply(IList<Object> objs, IList<Parameter> parameters, IValidationContext validationContext)
            {
                return objs.Any() && parameters.Any();
            }

            public bool Apply(IList<Object> objs, IList<Parameter> parameters, IValidationContext validationContext)
            {
                // convert to regexp syntax
                var convertedParams = parameters.Select(param => new Parameter { Value = ConvertToRegexp(param.Value) });
                return objs.All(obj => convertedParams.Any(param => StringMatchesPattern(obj.ToString(), param.Value)));
            }

            #endregion

            private bool StringMatchesPattern(String str, String pattern)
            {
                return Regex.IsMatch(str, pattern);
            }
        }

        class NotEqualsPredicate : IPredicate
        {
            #region IPredicate Membres

            private EqualsPredicate equals;

            public NotEqualsPredicate()
            {
                this.equals = new EqualsPredicate();
            }

            public bool CanApply(IList<Object> objs, IList<Parameter> parameters, IValidationContext validationContext)
            {
                return equals.CanApply(objs, parameters, validationContext);
            }

            public bool Apply(IList<Object> objs, IList<Parameter> parameters, IValidationContext validationContext)
            {
                return !equals.Apply(objs, parameters, validationContext);
            }

            #endregion
        }

        class NotEmptyPredicate : IPredicate
        {
            #region IPredicate Membres

            public bool CanApply(IList<Object> objs, IList<Parameter> parameters, IValidationContext validationContext)
            {
                return objs.Any() && !parameters.Any();
            }

            public bool Apply(IList<Object> objs, IList<Parameter> parameters, IValidationContext validationContext)
            {
                return objs.All(obj => obj != null && !String.IsNullOrEmpty(obj.ToString()));
            }

            #endregion
        }

        class MaxLengthPredicate : IPredicate
        {
            #region IPredicate Membres

            public bool CanApply(IList<Object> objs, IList<Parameter> parameters, IValidationContext validationContext)
            {
                int size;
                return objs.Any() && objs.All(obj => obj is String)
                        && parameters.Count == 1
                        && int.TryParse(parameters.Single().Value, out size)
                        && size > 0;
            }

            public bool Apply(IList<Object> objs, IList<Parameter> parameters, IValidationContext validationContext)
            {
                int size = int.Parse(parameters.Single().Value);
                return objs.All(obj => obj.ToString().Length <= size);
            }

            #endregion
        }

        class UnicityPredicate : IPredicate
        {
            #region IPredicate Membres

            public bool CanApply(IList<Object> objs, IList<Parameter> parameters, IValidationContext validationContext)
            {
                // Unicity pred is different than the others because it is a global predicate
                // It does not apply to a given model element
                // Se ElementRuleExt.ProcessUnicityGlobalRule
                return objs.All(obj => obj is String) 
                        // no pattern or a single pattern
                        && parameters.Count <= 1;
            }

            public bool Apply(IList<Object> objs, IList<Parameter> parameters, IValidationContext validationContext)
            {
                IEnumerable<String> listToTest;
                if (parameters.Count == 1)
                {
                    // Ex: objs contains: 'EXF-0001-Foo", "EXNF-0002-Bar", "EXF-0001-Baz"
                    // pattern is "*-%d%d%d%d - *"
                    listToTest = objs.Cast<String>().Select(str => ExtractUniquePart(str, parameters.Single().Value));
                }
                else
                {
                    listToTest = objs.Cast<String>();
                }

                return listToTest.Distinct().Count() == objs.Count;
            }

            #endregion

            /// <summary>
            /// Unique parts are just digits right now
            /// Ex: 
            /// str = "EXF-1234-Foo", pattern = "*-%d%d%d%d - *" => unique part = 1234
            /// str = "0001-Foobar-[42], pattern = "%d%d%d-*-[%d%d]" => unique part = 000142
            /// 
            /// </summary>
            /// <param name="str"></param>
            /// <param name="pattern"></param>
            /// <returns></returns>
            private String ExtractUniquePart(String str, String pattern)
            {
                // sanitize input
                String patternTmp = ConvertToRegexp(pattern);
                String uniquePart = str;

                // TODO

                return uniquePart;
            }
        }

        class RequiredPredicate : IPredicate
        {
            #region IPredicate Membres

            public bool CanApply(IList<Object> objs, IList<Parameter> parameters, IValidationContext validationContext)
            {
                return objs.Any();
            }

            public bool Apply(IList<Object> objs, IList<Parameter> parameters, IValidationContext validationContext)
            {
                // For each value in parameters, an obj with corresponding Type attribute exists
                return parameters.All(param => objs.Any(obj => param.Value.Equals(RetrieveTypeFromRequiredParam(obj))));
            }

            #endregion

            private String RetrieveTypeFromRequiredParam(Object param)
            {
                // TOOD: retrieve Type attribute by introspection
                return null;
            }
        }

        class ForbiddenPredicate : IPredicate
        {
            #region IPredicate Membres

            public bool CanApply(IList<Object> objs, IList<Parameter> parameters, IValidationContext validationContext)
            {
                return true;
            }

            public bool Apply(IList<Object> objs, IList<Parameter> parameters, IValidationContext validationContext)
            {
                bool ok;
                if (objs.Any())
                {
                    if (parameters != null && parameters.Any())
                    {
                        // find objects with forbidden stereotypes
                        ok = !objs.Any(obj => parameters.Any(param => param.Value.Equals(RetrieveStereotypeFromForbiddenParam(obj))));
                    }
                    else
                    {
                        // some object were found: forbidden
                        ok = false;
                    }
                }
                else
                {
                    // empty so nothing to forbid
                    ok = true;
                }

                return ok;
            }

            private String RetrieveStereotypeFromForbiddenParam(Object param)
            {
                // TOOD: retrieve Stereotype attribute by introspection
                return null;
            }

            #endregion
        }

        class ContainsPredicate : IPredicate
        {
            #region IPredicate Membres

            public bool CanApply(IList<Object> objs, IList<Parameter> parameters, IValidationContext validationContext)
            {
                return objs.Count == 1 && objs.Single() is String && parameters.Any();
            }

            public bool Apply(IList<Object> objs, IList<Parameter> parameters, IValidationContext validationContext)
            {
                var str = objs.Single().ToString();
                return parameters.Any(param => str.Contains(param.Value));
            }

            #endregion
        }

        class MaxSizePredicate : IPredicate
        {
            #region IPredicate Membres

            public bool CanApply(IList<Object> objs, IList<Parameter> parameters, IValidationContext validationContext)
            {
                int size;
                return objs.Count == 1 && objs.Single() is int
                        && parameters.Count == 1
                        && int.TryParse(parameters.Single().Value, out size)
                        && size > 0;
            }

            public bool Apply(IList<Object> objs, IList<Parameter> parameters, IValidationContext validationContext)
            {
                int count = Convert.ToInt32(objs.Single());
                int maxSize = int.Parse(parameters.Single().Value);
                return count <= maxSize;
            }

            #endregion
        }

        private static String ConvertToRegexp(String str)
        {
            return String.Format("^{0}$", Regex.Escape(str)
                                // Escape changes '*' to '\*' so we need to match that
                                .Replace(@"\*", ".*")
                                // Same here
                                .Replace(@"\?", ".")
                                .Replace("%d", @"\d"));
        }
    }
}
